package org.dftproject.genesis.core.agents;

import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dftproject.genesis.core.persistence.bdb.PersistenceBinding;
import org.eclipse.core.runtime.Platform;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.SynchronousBundleListener;

import com.sleepycat.je.Cursor;
import com.sleepycat.je.CursorConfig;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.DatabaseException;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.LockMode;
import com.sleepycat.je.OperationStatus;
import com.sleepycat.util.UtfOps;

public class AgentManager {

	private static final Log log = LogFactory.getLog(AgentManager.class);

	private static AgentManager singleton = new AgentManager();
	
	private final Map<String, IAgent> agentById = new HashMap<String, IAgent>();

	private final List<IAgentManagerListener> listeners = Collections.synchronizedList(new LinkedList<IAgentManagerListener>());

	private Environment environment;
	
	private AgentManager() {
		File environmentFile = new File(getPath());
		environmentFile.mkdirs();
	
		try {
			EnvironmentConfig environmentConfig = new EnvironmentConfig();
			environmentConfig.setAllowCreate(true);
			environment = new Environment(environmentFile, environmentConfig);
			
			init();
	
			Activator.getDefault().getBundle().getBundleContext().addBundleListener(new SynchronousBundleListener() {
	
				public void bundleChanged(BundleEvent event) {
					if (event.getType() == BundleEvent.STOPPING)
						close();
				}
	
			});
		} catch (DatabaseException e) {
			log.error("Failed to open agent manager environment", e);
		}
	}

	public static AgentManager getDefault() {
		return singleton;
	}
	
	public String getPath() {
		return Platform.getInstanceLocation().getURL().getPath().substring(1) + "agents";
	}
	
	protected void init() {
		Database database = null;
		Cursor cursor = null;

		try {
			database = getDatabase();
			if (database != null) {
				cursor = database.openCursor(null, CursorConfig.DEFAULT);
				if (cursor != null) {
					DatabaseEntry key = new DatabaseEntry();
					DatabaseEntry data = new DatabaseEntry();

					while (cursor.getNext(key, data, LockMode.DEFAULT) == OperationStatus.SUCCESS) {
						IAgent agent = (IAgent) new PersistenceBinding().entryToObject(data);
						if (agent != null) {
							agentById.put(agent.getId(), agent);
							agent.start();
						}
					}
				}
			}
		} catch (DatabaseException e) {
			log.error("Failed to get agents", e);
		} finally {
			try {
				if (cursor != null)
					cursor.close();
				if (database != null)
					database.close();
			} catch (DatabaseException e) {
			}
		}
	}
	
	public void close() {
		if (environment != null) {
			log.info("Closing the agent manager...");
			
			for (IAgent agent : agentById.values())
				agent.stop();
			
			agentById.clear();
			
			try {
				environment.close();
			} catch (DatabaseException e) {
				log.error("Failed to close agent manager environment", e);
			}
			
			environment = null;
		}
	}
	
	protected Database getDatabase() {
		try {
			DatabaseConfig databaseConfig = new DatabaseConfig();
			databaseConfig.setAllowCreate(true);
			return environment.openDatabase(null, "agents", databaseConfig);
		} catch (DatabaseException e) {
			log.error("Failed to open agent manager database", e);
		}
	
		return null;
	}
	
	public void addAgent(IAgent agent) {
		if (environment == null)
			return;
		
		if (agentById.containsKey(agent.getId())) {
			updateAgent(agent);
		} else {
			agentById.put(agent.getId(), agent);
			
			Database database = null;
	
			try {
				database = getDatabase();
				if (database != null) {				
					DatabaseEntry key = new DatabaseEntry(UtfOps.stringToBytes(agent.getId()));
					DatabaseEntry data = new DatabaseEntry();
					new PersistenceBinding().objectToEntry(agent, data);
					
					database.put(null, key, data);
				}
			} catch (Exception e) {
				log.error("Failed to store agent '" + agent.getId() + "'", e);
			} finally {
				try {
					if (database != null)
						database.close();
				} catch (DatabaseException e) {
				}
			}
			
			notifyAgentAdded(agent);
		}
	}
	
	public void updateAgent(IAgent agent) {
		if (environment == null)
			return;
		
		if (!agentById.containsKey(agent.getId())) {
			addAgent(agent);
		} else {
			agentById.put(agent.getId(), agent);
		
			Database database = null;
	
			try {
				database = getDatabase();
				if (database != null) {				
					DatabaseEntry key = new DatabaseEntry(UtfOps.stringToBytes(agent.getId()));
					DatabaseEntry data = new DatabaseEntry();
					new PersistenceBinding().objectToEntry(agent, data);
					
					database.put(null, key, data);
				}
			} catch (Exception e) {
				log.error("Failed to store agent '" + agent.getId() + "'", e);
			} finally {
				try {
					if (database != null)
						database.close();
				} catch (DatabaseException e) {
				}
			}
			
			notifyAgentUpdated(agent);
		}
	}
	
	public void removeAgent(IAgent agent) {
		if (environment == null)
			return;
		
		agentById.remove(agent.getId());
		
		Database database = null;

		try {
			database = getDatabase();
			if (database != null) {				
				DatabaseEntry key = new DatabaseEntry(UtfOps.stringToBytes(agent.getId()));
				database.delete(null, key);
			}
		} catch (Exception e) {
			log.error("Failed to remove agent '" + agent.getId() + "'", e);
		} finally {
			try {
				if (database != null)
					database.close();
			} catch (DatabaseException e) {
			}
		}
		
		notifyAgentRemoved(agent);
	}
	
	public Set<IAgent> getAgents() {
		if (environment == null)
			return Collections.emptySet();
		
		return new HashSet<IAgent>(agentById.values());
	}

	public IAgent getAgent(String id) {
		if (environment == null)
			return null;
		
		return agentById.get(id);
	}

	public void addListener(IAgentManagerListener listener) {
		listeners.add(listener);
	}

	public void removeListener(IAgentManagerListener listener) {
		listeners.remove(listener);
	}

	protected void notifyAgentAdded(IAgent agent) {
		for (IAgentManagerListener listener : listeners)
			listener.agentAdded(agent);
	}

	protected void notifyAgentUpdated(IAgent agent) {
		for (IAgentManagerListener listener : listeners)
			listener.agentUpdated(agent);
	}

	protected void notifyAgentRemoved(IAgent agent) {
		for (IAgentManagerListener listener : listeners)
			listener.agentRemoved(agent);
	}

}
